經過這幾天的重構整理,看起來越來越有點樣子了,雖然剩下 7 天(扣除掉最後一天完賽宣言衝篇數),但其實還有很多功能還沒實作完成,尤其是看到 application.rb
裡面 call
那段程式碼目前的處理,就覺得還是很母湯,那就繼續一步一步看怎麼改善吧!
還記得 Active Record 和 Active Support 的架構嗎?Action Controller 也是一樣,先建立一個 base.rb
的檔案
# mavericks/lib/action_controller/base.rb
module ActionController
class Base
end
end
接著將原先的 controller.rb
裡面的程式碼搬移到 base.rb
底下
因為原先的 controller.rb
有用到 erubi
,所以連同 action_contorller
要一起寫進 all.rb
裡面
# mavericks/lib/mavericks/all.rb
require 'erubi'
require 'yaml'
require "mavericks"
require "active_support"
require "active_record"
require "action_controller"
require "mavericks/routing"
# 原先的 controller 可以拿掉
# require "mavericks/controller"
接著一樣要建立 action_controller.rb
做 autoload
# mavericks/lib/action_controller.rb
module ActionController
autoload :Base, "action_controller/base"
end
最後別忘了 just_do 的 TasksController
要改繼承 ActionController::Base
# just_do/app/controllers/tasks_controller.rb
class TasksController < ActionController::Base
def index
@tasks = Task.all
end
def show
@task = Task.find(params['id'])
end
end
另外前面我們曾經實作過 Controller 呼叫 Action 的功能,但其實這個做法會有些問題,會不好處理 before_action
之類的問題,所以為了要實作 before_action
,我們需要用一個 method 將他包起來處理
# mavericks/lib/mavericks/application.rb
# (略)
# .
# .
def call
klass, act = get_controller_and_action(env)
controller = klass.new(env)
# 原本的寫法
# controller.send(act)
# 改成這個
controller.process(act)
# .
# .
# (略)
# mavericks/lib/action_controller/base.rb
module ActionController
class Base
attr_reader :env, :content
def initialize(env)
@env = env
@content = nil
end
# 加上一個 process 來執行 Action
def process(action)
send action
end
# .
# .
# (略)
這樣就完成基本架構,接著就看看要加上那些功能吧!
在實作 before_action
,我們先來看看 Rails 的 before_action 寫法
# just_do/app/controllers/tasks_controller.rb
class TasksController < ActionController::Base
before_action :find_task, only:[:show]
def index
@tasks = Task.all
end
def show; end
private
def find_task
@task = Task.find(params['id'])
end
end
我們預期希望像 Rails 一樣有一個 before_action
的 callback,可以讓我們在指定的 Action 之前先處理好一些事情,為了要做到這樣的效果,我們需要在 Controller 上面建立一個 before_action
的 class method,因為 callback 不只是只有 before_action,所以我們勢必要做一個 callback 的 module
# mavericks/lib/action_controller/callbacks.rb
module ActionController
module Callbacks
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def before_action(method, options={})
# TODO
end
end
end
end
接著只要在 base.rb
裡面 include Callbacks
就可以有...等等,為什麼是用 include Callbacks
?而不是 extend Callbacks
?
其實你仔細看 callbacks module
,長的跟平常的 module 有點不太一樣,這裡我們用了 Ruby 的 Hook Methods
的技巧來實作Metaprogramming
所提到的 The Include-and-Extend Trick
,這樣的方式可以讓我們在 include 時,同時新增 instance method 和 class method,在 Rails 這個 framework 裡面是 是一個很常見的技巧
仔細看 self.included
的部分
def self.included(base)
base.extend ClassMethods
end
這就是剛剛所提到的 Hook Methods
,在呼叫 include 的時候,會觸發這個方法,其中參數 base 就是看那個 Class 觸發了這個 Hook,而裡面 base.extend ClassMethods
才是我們真正實作 extend method 的地方,至於 ClassMethods 其實也就是一個 module,裡面包含了一個 before_action
在了解怎麼同時 include 和 extend method 之後,接著我們就繼續實作 before_action 的部分,不過在實作前,我們還要再聊聊另一個觀念,就是關於 Ruby 的 super
,
class Parent
def say
puts "I am parent"
end
end
module A
def say
puts "I am A"
super
end
end
module B
def say
puts "I am B"
super
end
end
class Parent
include A, B
end
Parent.new.say
上面這個例子我們有一個 class Parent,為了要加入 module A
和 B
的方法,在底下做 Open Classes
並且 include A
和 B
,預期希望會是這樣的結果
I am A
I am B
I am parent
但實際上只有
I am parent
為什麼?
那是因為 Ruby 有繼承鏈的概念,什麼意思?我們可以在剛剛的程式碼底下加上這段程式碼
puts Parent.ancestors
你會發現這串東西,這個就是所謂的繼承鏈
Parent
A
B
Object
Kernel
BasicObject
這也是 Ruby 在找尋「方法」時的順序,也就是說 Ruby 在找 say
這個方法時,會先在 Parent
裡面找到,而在 Parent
裡面的 say
並沒有呼叫 super
,所以 Ruby 執行完 Parent
的 say
以後就不會再執行 A
和 B
裡面的 say
反過來說,如果我們想辦法把 A
和 B
擺在 Parent
,是不是就可以做到我們要的擴充方法效果?
是的,Ruby 剛好就有提供這樣的方法,我們把 include
換成 prepend
class Parent
prepend A, B
end
接著再執行一次,就會發現是我們要的結果,再檢查一次繼承鏈
A
B
Parent
Object
Kernel
BasicObject
會發現 A
和 B
確實排在 Parent
前面,也因為 A
和 B
都有呼叫 super,所以就會繼續執行 Parent
裡面的程式碼
但是其實還有另一個做法,就是建立一個子類別來 include A
和 B
,像是這樣
class Parent
def say
puts "I am parent"
end
end
module A
def say
puts "I am A"
super
end
end
module B
def say
puts "I am B"
super
end
end
# 建立一個子類別
class Child < Parent
include A, B
end
puts Child.ancestors
你可以看到繼承鏈變成這樣
Child
A
B
Parent
Object
Kernel
BasicObject
利用這樣的技巧,我們就可以在子類別做 include,來擴充父類別的方法,一種簡單用乾淨的做法,會提到這個是因為 Rails 也是利用這個技巧,來實作擴充的效果,明天就會繼續來實作這部分
Mavericks 程式碼
https://github.com/apayu/mavericks